#include "StateLoader.h"
#include <SECore/Reflect.h>
#include <SEngine/Ecs/TypeRegister.h>
#include <SEngine/Ecs/EntityLoader/SceneLoader.h>
#include <SEngine/Ecs/EntityLoader/ActorLoader.h>

#include <SECore/Events/Input.h>

namespace SEngine
{
    template<typename T>
    void UsertypeBind(sol::state & lua, T* = nullptr) {
        Reflect<T> info;
        auto & type = lua.new_usertype<T>(info.Name());
        info.ForeachField([&](auto field) {
            type.set(field.name, field.fieldPtr);
            if constexpr (REF_IS_REFLECTABLE(field)) {
                UsertypeBind<decltype(field)::Type>(lua, nullptr);
            } 
        });
        info.ForeachFunc([&](auto func) {
            type.set_function(func.name, func.ptr);
        });
    }

    template<typename T>
    void VecFunctions(sol::usertype<T> & type, sol::state & lua) {
        Reflect<T> info;
        type.set_function(sol::meta_function::equal_to, static_cast<T&(T::*)(T const &)>(&T::operator=));
        type.set_function(sol::meta_function::addition, [](T t, T v) { return t + v; });
        type.set_function(sol::meta_function::subtraction, [](T t, T v) { return t - v; });
        type.set_function(sol::meta_function::multiplication, sol::overload(
            static_cast<T&(T::*)(T const &)>(&T::operator*=),
            static_cast<T&(T::*)(float const &)>(&T::operator*=)
        ));
        type.set_function(sol::meta_function::division, sol::overload(
            static_cast<T&(T::*)(T const &)>(&T::operator/=),
            static_cast<T&(T::*)(float const &)>(&T::operator/=)
        ));
        info.ForeachField([&](auto field) {
            type.set(field.name, field.fieldPtr);
        });
        std::string name = info.Name();
        name[0] = 'V';                  // vec -> Vec
        lua.create_named_table(name);
        lua[name]["normalize"] = [] (T & v) { return glm::normalize(v); };
    }

    template<typename T>
    void UsertypeBind(sol::state & lua, 
        std::enable_if_t<std::is_same_v<T, glm::vec1>, T> * = nullptr) {
        Reflect<T> info;
        auto & type = lua.new_usertype<T>(info.Name(), sol::constructors<
            T(), 
            T(float),
            T(glm::vec2)
            >()
        );
        VecFunctions(type, lua);
    }   
    
    template<typename T>
    void UsertypeBind(sol::state & lua, 
        std::enable_if_t<std::is_same_v<T, glm::vec2>, T> * = nullptr) {
        Reflect<T> info;
        auto & type = lua.new_usertype<T>(info.Name(), sol::constructors<
            T(), 
            T(float),
            T(float, float),
            T(glm::vec3)
            >()
        );
        VecFunctions(type, lua);
    }   
    
    template<typename T>
    void UsertypeBind(sol::state & lua, 
        std::enable_if_t<std::is_same_v<T, glm::vec3>, T> * = nullptr) {
        Reflect<T> info;
        auto & type = lua.new_usertype<T>(info.Name(), sol::constructors<
            T(), 
            T(float),
            T(float, float, float),
            T(glm::vec4)
            >()
        );
        VecFunctions(type, lua);
    }   

    template<typename T>
    void UsertypeBind(sol::state & lua, 
        std::enable_if_t<std::is_same_v<T, glm::vec4>, T> * = nullptr) {
        Reflect<T> info;
        auto & type = lua.new_usertype<T>(info.Name(), sol::constructors<
            T(), 
            T(float),
            T(float, float, float, float),
            T(glm::vec3, float)
            >()
        );
        VecFunctions(type, lua);
    }   

    template<typename T>
    void UsertypeBind(sol::state & lua, 
        std::enable_if_t<std::is_same_v<T, glm::mat4>, T> * = nullptr) {
        auto & type = lua.new_usertype<T>("mat4",
            sol::constructors<T(), T(float)>(),
            sol::meta_function::equal_to, static_cast<T&(T::*)(T const &)>(&T::operator=)
        );
        lua.create_named_table("Mat4");
        lua["Mat4"]["dot"] = [](glm::mat4 m, glm::vec4 v) { return m * v; };
    }

    template<typename T>
    void UsertypeBind(sol::state & lua, 
        std::enable_if_t<std::is_same_v<T, glm::quat>, T> * = nullptr) {
        Reflect<T> info;
        auto & type = lua.new_usertype<T>(info.Name(), sol::constructors<
            T(glm::vec3), 
            T(float, float, float, float)
            >()
        );
        type.set_function(sol::meta_function::addition, [](glm::quat q1, glm::quat q2) { return q1 + q2; });
        type.set_function(sol::meta_function::subtraction, [](glm::quat q1, glm::quat q2) { return q1 - q2; });
        type.set_function(sol::meta_function::multiplication, [](glm::quat q1, glm::quat q2) { return q1 * q2; });
        info.ForeachField([&](auto field) {
            type.set(field.name, field.fieldPtr);
            if constexpr (REF_IS_REFLECTABLE(field)) {
                UsertypeBind<decltype(field)::Type>(lua, nullptr);
            } 
        });
        info.ForeachFunc([&](auto func) {
            type.set_function(func.name, func.ptr);
        });
    }

    void RegisterMath(sol::state & lua) {
        lua.create_named_table("SEMath");
        lua["SEMath"]["Radians"] = sol::overload(
            [](float angle) { return glm::radians(angle); }, 
            [](glm::vec2 angle) { return glm::radians(angle); }, 
            [](glm::vec3 angle) { return glm::radians(angle); }, 
            [](glm::vec4 angle) { return glm::radians(angle); } 
        );
        lua["SEMath"]["Degrees"] = sol::overload(
            [](float angle) { return glm::degrees(angle); }, 
            [](glm::vec2 angle) { return glm::degrees(angle); }, 
            [](glm::vec3 angle) { return glm::degrees(angle); }, 
            [](glm::vec4 angle) { return glm::degrees(angle); } 
        );
        lua["SEMath"]["Rotate"] = sol::overload(
            [](glm::mat4& m, float angle, glm::vec3& axis) { return glm::rotate(m, angle, axis); },
            [](glm::quat& q, float angle, glm::vec3& axis) { return glm::rotate(q, angle, axis); }
        );
        lua["SEMath"]["EulerAngles"] = [](glm::quat & q) { return glm::eulerAngles(q); };
        lua["SEMath"]["QuatAngles"] = [](glm::vec3 & v) { return glm::quat(v); };
        UsertypeBind<glm::vec1>(lua, nullptr);
        UsertypeBind<glm::vec2>(lua, nullptr);
        UsertypeBind<glm::vec3>(lua, nullptr);
        UsertypeBind<glm::vec4>(lua, nullptr);
        UsertypeBind<glm::mat4>(lua, nullptr);
        UsertypeBind<glm::quat>(lua, nullptr);
    }

    void RegisterScene(sol::state & lua)
    {
        lua.new_usertype<SceneLoader>("Scene", 
            "CreateActor", &SceneLoader::CreateActor,
            "DestroyActor", &SceneLoader::DestroyActor
        );
    }

    void RegisterActor(sol::state & lua)
    {
        auto & type = lua.new_usertype<ActorLoader>("Actor",
            "GetScene", &ActorLoader::GetScene
        );    
        ForeachType([&type, &lua](auto loader) {
            UsertypeBind<decltype(loader)::Type>(lua);
            Reflect<decltype(loader)::Type> info;
            type.set_function("Get" + info.Name(), &ActorLoader::GetComponent<decltype(loader)::Type>);
            type.set_function("Has" + info.Name(), &ActorLoader::HasComponent<decltype(loader)::Type>);
            type.set_function("Add" + info.Name(), &ActorLoader::AddComponent<decltype(loader)::Type>);
        }, TypesLoader<TransformComp, CameraComp>());
    }

    void RegisterInputs(sol::state & lua) {
        lua.new_enum<MouseButton>("MouseButton", 
        {
            {"Left", MouseButton::SE_LEFT},
            {"Mid", MouseButton::SE_MID},
            {"Right", MouseButton::SE_RIGHT}
        });
        lua.create_named_table("Inputs");
        lua["Inputs"]["Key"] = [](char key) { return (uint8_t)key; };
        lua["Inputs"]["IsKeyPressed"] = [](uint8_t key) {
            return Input::GetInstance().IsKeyPressed(key);
        };
        lua["Inputs"]["IsMousePressed"] = [](uint8_t key) {
            return Input::GetInstance().IsMousePressed((MouseButton)key);
        };
        lua["Inputs"]["GetMousePos"] = []() {
            auto pt = Input::GetInstance().GetMousePos();
            return glm::vec2{pt.first, pt.second};
        };
        lua["Inputs"]["GetMouseWheelXY"] = []() {
            auto pt = Input::GetInstance().GetMouseWheelXY();
            return glm::vec2{pt.first, pt.second};
        };
    }

    StateLoader::StateLoader()
    {
        Register(RegisterMath);
        Register(RegisterScene);
        Register(RegisterActor);
        Register(RegisterInputs);
    }

    StateLoader &StateLoader::GetInstance()
    {
        static StateLoader instance;
        return instance;
    }

    std::unique_ptr<sol::state> StateLoader::CreateState()
    {
        auto lua = std::make_unique<sol::state>();
        lua->open_libraries(sol::lib::base);
        lua->open_libraries(sol::lib::math);
        for (auto & fn : m_registerTable) {
            fn(*lua);
        }
        return lua;
    }

} // namespace SEngine
